// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: jhkim

#include "coin2/exchange/sim/order_v1/matching_engine/matching_engine.h"

#include "coin2/base/string_util.h"

namespace coin2::exchange::sim::order_v1 {

void SymbolMatchingEngine::SubmitCancel(int64_t order_id, int64_t cancel_submit_time) {
  publisher_.PublishCancelSubmitted(order_id, time, cancel_submit_time);
}

void SymbolMatchingEngine::SubmitAcceptOrder(
    int64_t order_id,
    int64_t submit_time,
    double price,
    double qty,
    int sign,
    bool post_only,
    std::string tag,
    OrderRecordProto::RecordType record_type,
    std::optional<double> submit_max_rate_used_ratio,
    std::optional<double> cancel_max_rate_used_ratio) {
  publisher_.PublishSubmitAcceptOrder(
      order_id,
      time,
      submit_time,
      price,
      qty,
      sign,
      post_only,
      tag,
      record_type,
      submit_max_rate_used_ratio,
      cancel_max_rate_used_ratio);
}

const std::vector<OrderRecordProto>& SymbolMatchingEngine::GetOrderRecords() const {
  return publisher_.GetOrderRecords();
}

void SymbolMatchingEngine::ClearOrderRecords() { publisher_.ClearOrderRecords(); }

void SymbolMatchingEngine::UpdateFeed(
    const Symbol* symbol, const ::fastfeature::FeedMessage& feed) {
  if (feed.feed_type() == ::fastfeature::BOOK_FEED) {
    prebook = std::move(book);
    book.reset(new LightBook_10_dec12(use_exchange_timestamp_));
    book->UpdateBook(feed);
    if (feed.book().ask_size() > 0 && feed.book().bid_size() > 0) {
      mtm_price = 0.5 * (feed.book().ask(0).price_float() + feed.book().bid(0).price_float());
      double bboq = 0.5 * (feed.book().ask(0).qty_float() + feed.book().bid(0).qty_float());
      bboq_window.Update(feed.fetched_time(), bboq);
    }
  }
  if (feed.feed_type() == ::fastfeature::TRADE_FEED) {
    book->UpdateFromTrade(feed, true);
  }
  double trade_filled_qty = 0;
  for (auto* orders : {&buys, &sells}) {
    for (auto&& qs : *orders) {
      if (qs->alive) {
        if (!qs->accepted) {
          const LightBook_10_dec12* last_known_book =
              (prebook->Ready()) ? prebook.get() : book.get();
          if (!last_known_book->IsBidBookAvail()
              || !last_known_book->IsAskBookAvail()) {
            continue;
          }
          CHECK(last_known_book->Ready()) << symbol->symbol_string() << " book not ready";
          if (last_known_book->IsBidBookAvail()) {
            qs->bid0_price_on_accepted = last_known_book->bid_price(0);
          }
          if (last_known_book->IsAskBookAvail()) {
            qs->ask0_price_on_accepted = last_known_book->ask_price(0);
          }

          if (is_snapshot) {
            if (feed.feed_type() == ::fastfeature::BOOK_FEED) {
              // can't match with prebook directly(too naive). we must interpolate
              qs->AcceptOrder(last_known_book, *book);
            } else {
              // defer accept until next book is available
              continue;
            }
          } else {
            // match with prebook(== last_known_book). cuz it's last known valid book
            qs->AcceptOrder(nullptr, *last_known_book);
          }
          // accepted or rejected
          AcceptQueueStatus(qs.get(), feed.fetched_time());
          if (qs->alive) {
            CHECK(qs->accepted);
          } else {
            continue;
          }
        }
        qtrack->ApplyFeed(feed, qs);
        if (qs->queue_pos < 0 && qs->remain_qty > 0) {
          double filled_qty = std::min(-(qs->queue_pos), qs->remain_qty);
          qs->queue_pos += filled_qty;
          if (feed.feed_type() == ::fastfeature::TRADE_FEED) {
            trade_filled_qty += filled_qty;
            double subtract_qty = 0;
            if (trade_filled_qty > feed.trade().qty_float()) {
              subtract_qty = trade_filled_qty - feed.trade().qty_float();
            }
            filled_qty -= subtract_qty;
            trade_filled_qty -= subtract_qty;
          }
          if (filled_qty > 0) {
            qs->AddFill(qs->price, filled_qty);
            publisher_.PublishFill(
                qs->order_id,
                feed.fetched_time(),
                qs->sign,
                qs->price,      // order_price
                qs->order_qty,  // order_qty
                qs->post_only,
                qs->tag,
                OrderRecordProto::MAKER,  // fill_type
                qs->price,                   // fill price
                filled_qty);
          }
          position += filled_qty * qs->sign;
          if (qs->remain_qty <= 0) {
            qs->alive = false;
          }
        }
        if (::fastfeature::TRADE_FEED && trade_filled_qty >= feed.trade().qty_float()) {
          break;
        }
      }
    }
    if (::fastfeature::TRADE_FEED && trade_filled_qty > feed.trade().qty_float()) {
      break;
    }
  }
  ClearDeadOrders();
}

// send order
void SymbolMatchingEngine::SendOrder(
    int64_t order_arrival_time,
    const PriceType& price,
    double order_qty,
    int sign,
    bool post_only,
    std::string tag,
    bool ioc,
    int64_t order_id) {
  auto qs = std::make_shared<QueueStatus>(
      order_arrival_time,
      order_id,
      sign,
      post_only,
      tag,
      ioc,
      makerfill_tolerance_bps,
      price,
      order_qty,
      order_qty,
      1000000000LL,  // infinite queue pos
      0.,
      0.,
      nullptr,
      ::fastfeature::UNKNOWN_FEED);
  auto* sidequeue = (sign == 1) ? &buys : &sells;
  sidequeue->insert(qs);
  map_on_oid[order_id] = qs;
  // order arrived. not accepted
  CHECK(!qs->accepted);
}

// cancel order
void SymbolMatchingEngine::CancelOrder(int64_t order_id, int64_t cancel_arrival_time) {
  if (map_on_oid.count(order_id)) {
    auto& qs = map_on_oid.at(order_id);
    if (qs->ioc) return;
    qs->cancel_arrival_time = cancel_arrival_time;
    // HBDM: in snapshot, defer cancel and handle the cancellation in the UpdateFeed() routine
    if (!is_snapshot) {
      qs->cancel_confirmed_time = cancel_arrival_time;
    }
  } else {
    publisher_.PublishCancelError(order_id, time, cancel_arrival_time, 0, 0);
  }
}

double SymbolMatchingEngine::GetPosition() const { return position; }

void SymbolMatchingEngine::SetExchangeTime(int64_t time) {
  this->time = std::max(this->time, time);
}

void SymbolMatchingEngine::SetFetchedTime(int64_t ftime) {
  this->ftime = std::max(this->ftime, ftime);
}

int64_t SymbolMatchingEngine::GetFetchedTime() const { return this->ftime; }


void SymbolMatchingEngine::AcceptQueueStatus(QueueStatus* qs, int64_t timestamp) {
  if ((qs->post_only && qs->taker_fill.fill_qty > 0) || qs->order_qty == 0) {
    // ignore post only order when it has taker fills.
    // and ignore the taker fills as well.
    qs->alive = false;
    // reject order.

    publisher_.PublishOrderError(
        qs->order_id,
        time,
        timestamp,
        qs->price,
        qs->order_qty,
        qs->sign);
  } else {
    if (qs->taker_fill.fill_qty > 0) {
      qs->AddFill(qs->taker_fill.fill_price(), qs->taker_fill.fill_qty);
      publisher_.PublishFill(
          qs->order_id,
          timestamp,
          qs->sign,
          qs->price,      // order_price
          qs->order_qty,  // order_qty
          qs->post_only,
          qs->tag,
          OrderRecordProto::TAKER,  // fill_type
          qs->taker_fill.fill_price(),
          qs->taker_fill.fill_qty);
      position += qs->taker_fill.fill_qty * qs->sign;
      CHECK(!qs->post_only);
    }

    if (qs->remain_qty <= 0) {
      qs->alive = false;
    } else if (qs->ioc) {
      publisher_.PublishCancelConfirm(
          qs->order_id,
          time,
          timestamp,
          qs->price,
          qs->remain_qty,
          qs->sign);
      qs->cancel_arrival_time = timestamp;
      qs->cancel_confirmed_time = timestamp;
      qs->alive = false;
    }
  }
}

void SymbolMatchingEngine::ClearDeadOrders() {
  // just one more necessary iteration. it won't hurt too much.
  for (auto* orders : {&buys, &sells}) {
    for (auto it = orders->begin(); it != orders->end();) {
      if ((*it)->cancel_arrival_time > 0) {
        // cancel confirmed, or dead for another reason
        if (!(*it)->alive || (*it)->cancel_confirmed_time > 0) {
          CancelOrderActual((*it)->order_id, (*it)->cancel_confirmed_time);
          CHECK(!(*it)->alive);
        }
      }
      if (!(*it)->alive) {
        dead_orders.push_back(*it);
        it = orders->erase(it);
      } else {
        it++;
      }
    }
  }
}

void SymbolMatchingEngine::CancelOrderActual(int64_t order_id, int64_t cancel_confirm_time) {
  bool cancelled = false;
  double order_price = 0;
  int sign = 0;

  if (map_on_oid.count(order_id)) {
    auto& qs = map_on_oid.at(order_id);
    CHECK_EQ(qs->order_id, order_id);
    order_price = qs->price;
    sign = qs->sign;
    if (qs->order_id == order_id && qs->alive) {
      qs->alive = false;
      publisher_.PublishCancelConfirm(
          order_id,
          time,
          cancel_confirm_time,
          qs->price,
          qs->remain_qty,
          qs->sign);
      qs->remain_qty = 0;
      cancelled = true;
    }
  }
  if (!cancelled) {
    publisher_.PublishCancelError(
        order_id,
        time,
        cancel_confirm_time,
        order_price,
        sign);
  }
}

void MatchingEngine::UpdateFeed(const Symbol* symbol, const ::fastfeature::FeedMessage& feed) {
  if (acct_config_.use_exchange_timestamp()
      && feed.has_exchange_time()
      && feed.exchange_time() > 0) {
    holder.SetExchangeTime(feed.exchange_time());
    holder.SetFetchedTime(feed.fetched_time());
    job_queue.UpdateTimeOnly(feed.exchange_time());
  } else {
    holder.SetExchangeTime(feed.fetched_time());
    holder.SetFetchedTime(feed.fetched_time());
    job_queue.UpdateTimeOnly(feed.fetched_time());
  }
  holder.UpdateFeed(symbol, feed);
}

void MatchingEngine::SendOrder(
    int64_t given_order_id,
    std::optional<int64_t> last_time_given,
    double price,
    double qty,
    int sign,
    bool post_only,
    std::string tag,
    bool ioc,
    std::optional<double> submit_max_rate_used_ratio,
    std::optional<double> cancel_max_rate_used_ratio) {
  int64_t last_time = last_time_given ? *last_time_given : holder.GetFetchedTime();
  // LOG(INFO) << "last_time: " << last_time;
  {
    auto type = OrderRecordProto::ORDER_SUBMITTED;
    holder.SubmitAcceptOrder(
        given_order_id,
        last_time,
        price,
        qty,
        sign,
        post_only,
        tag,
        type,
        submit_max_rate_used_ratio,
        cancel_max_rate_used_ratio
        );
  }

  {
    auto ts = last_time + submit_to_accept_latency_ns;
    auto type = OrderRecordProto::ORDER_ACCEPTED;
    std::function<void()> job = std::bind(
        &SymbolMatchingEngine::SubmitAcceptOrder,
        &holder,
        given_order_id,
        ts,
        price,
        qty,
        sign,
        post_only,
        tag,
        type, 
        submit_max_rate_used_ratio,
        cancel_max_rate_used_ratio);
    job_queue.PushElement(ts, last_time, job);
  }
  {
    auto ts = last_time + submit_to_first_fill_latency_ns;
    std::function<void()> job = std::bind(
        &SymbolMatchingEngine::SendOrder,
        &holder,
        ts,
        PriceType(price),
        qty,
        sign,
        post_only,
        tag,
        ioc,
        given_order_id);
    job_queue.PushElement(ts, last_time, job);
  }
}

void MatchingEngine::CancelOrder(int64_t order_id) {
  int64_t last_time = holder.GetFetchedTime();
  int64_t ts = last_time + cancel_to_confirm_latency_ns;
  std::function<void()> job =
      std::bind(&SymbolMatchingEngine::CancelOrder, &holder, order_id, ts);
  holder.SubmitCancel(order_id, last_time);
  job_queue.PushElement(ts, last_time, job);
}

}  // namespace coin2::exchange::sim::order_v1
